VimのNormal modeを実装してみる
どういうコードを書けば動くのか試してみるtakker.icon
設計とかは全然考えていません
(最終的にはclassで設計し直すつもり)
MousetrapやnativeのAPIを使ってどうcodingすればkey bindingを実現できるかを試しています 借らなくても、もっといい感じにできるかな?
既知の問題
ddなどの連続したキー入力で、最後の文字以外が入力されてしまう
別途MousetrapOnEdit.bind('d',e=>{...})などで1文字目を入力しないようにしてしまうと、dが押されなかったことになってしまう
Event.StopPropagation()を削ってもうまく行かない
Mousetrapを使わずに、自前で入力されたkeyをstackするsystemを作ったほうが早いように思うtakker.icon code:prototype1.js
import {installMousetrap} from '/api/code/villagepump/scrapbox-mousetrap-installer/script.js';
installMousetrap();
// 本体をinstallする
const oldScript = document.getElementById('scrapbox-click-link');
oldScript?.parentNode.removeChild(oldScript);
const script = document.createElement("script");
script.src = '/api/code/villagepump/Normal modeを実装してみる/script.js';
script.id = 'scrapbox-click-link';
script.type = 'module';
document.body.appendChild(script);
const cssId = 'scrapbox-normal-mode';
let style = document.getElementById(cssId);
//if (style){ style.remove() }
style?.remove()
document.head.insertAdjacentHTML('beforeend',`
<style id="${cssId}">
@import "/api/code/villagepump/Normal_modeを実装してみる/cursor.css";
</style>
`);
Normal modeのカーソルの形
とりあえず区別できるようにした
理想は1文字分ハイライトされること
code:cursor.css
.cursor.normal-mode {
width: 4px !important;
background-color: rgb(57, 172, 134);
}
/* 黒い線が気になるのでカーソルと同じ色にする */
.cursor.normal-mode {f
color: rgb(57, 172, 134);
border-color: rgb(57, 172, 134);
}
.cursor.normal-mode svg {
display: none;
}
code:script.js
import {getLinkIncludingCursor} from '/api/code/takker/Scrapboxでcursor下のリンクを取得する/script.js';
const editor = document.getElementById('editor');
const cursor = document.getElementById('text-input');
let mode = '';
moveNormalMode();
const moustrapOnEdit = new Mousetrap(editor);
yank
code:script.js
moustrapOnEdit.bind('y y', e =>{
_log(${e.key} is pressed.);
if (mode !== 'normal') return;
e.stopPropagation();
e.preventDefault();
const cursorLine = editor.getElementsByClassName('cursor-line')0; if(navigator.clipboard){
navigator.clipboard.writeText(cursorLine.innerText);
}
});
特殊なキーと文字を握りつぶす
code:script.js
moustrapOnEdit.bind(['left','up','down','right',
'ctrl+left','ctrl+up','ctrl+down','ctrl+right',
'alt+left','alt+up','alt+down','alt+right',
'del','backspace','enter',], e =>{
_log(${e.key} is pressed.);
if (mode !== 'normal') return;
_log(${e.key} will be prevented from inserted.);
e.stopPropagation();
e.preventDefault();
});
moustrapOnEdit.bind('1234567890-\\qwertyd@sf;:zcvbnm,.'.split(''), e =>{ _log(${e.key} is pressed.);
if (mode !== 'normal') return;
_log(${e.key} will be prevented from inserted.);
e.preventDefault();
});
検知できていない?
Mousetrapの対象を#text-inputに変えてみる
検知できなかった
Mousetrapのバグか?
仕方ないのでaddEventListenerで握りつぶす
ファイル名を間違えていただけだった……
scrpt.js→script.js
mode切り替え
code:script.js
moustrapOnEdit.bind('i', e =>{
_log(${e.key} is pressed.);
if (mode !== 'normal') return;
e.stopPropagation();
e.preventDefault();
moveInsertMode();
});
moustrapOnEdit.bind('a', e =>{
_log(${e.key} is pressed.);
if (mode !== 'normal') return;
e.stopPropagation();
e.preventDefault();
simulateKeyPress('right');
moveInsertMode();
});
moustrapOnEdit.bind('I', e =>{
_log(${e.key} is pressed.);
if (mode !== 'normal') return;
e.stopPropagation();
e.preventDefault();
simulateKeyPress('home');
moveInsertMode();
});
moustrapOnEdit.bind('A', e =>{
_log(${e.key} is pressed.);
if (mode !== 'normal') return;
e.stopPropagation();
e.preventDefault();
simulateKeyPress('end');
moveInsertMode();
});
新しい行を作る
code:script.js
moustrapOnEdit.bind('o', e =>{
_log(${e.key} is pressed.);
if (mode !== 'normal') return;
e.stopPropagation();
e.preventDefault();
simulateKeyPress('end');
simulateKeyPress('enter');
moveInsertMode();
});
<C-[>が押されたらnormal modeに移行
code:script.js
moustrapOnEdit.bind(['ctrl+','j j', e => { e.stopPropagation();
e.preventDefault();
if (mode !== 'insert') return;
moveNormalMode();
});
jjでもnormal modeに移行するようにしてみる
うまく動かない
code:script.js
moustrapOnEdit.bind('j j', e => {
e.stopPropagation();
e.preventDefault();
if (mode !== 'insert') return;
moveNormalMode();
});
cursor移動とか
code:script.js
moustrapOnEdit.bind('hjkl^$'.split(''), e =>{
_log(${e.key} is pressed.);
if (mode !== 'normal') return;
e.stopPropagation();
e.preventDefault();
let key = '';
switch(e.key) {
case 'h':
key = 'left';
break;
case 'j':
key = 'down';
break;
case 'k':
key = 'up';
break;
case 'l':
key = 'right';
break;
case '^':
key = 'home';
break;
case '$':
key = 'end';
break;
}
simulateKeyPress(key);
});
code:script.js
moustrapOnEdit.bind('ctrl+u', e =>{
_log(${e.key} is pressed.);
if (mode !== 'normal') return;
e.stopPropagation();
e.preventDefault();
simulateKeyPress('pageup');
});
moustrapOnEdit.bind('ctrl+f', e =>{
_log(${e.key} is pressed.);
if (mode !== 'normal') return;
e.stopPropagation();
e.preventDefault();
simulateKeyPress('pagedown');
});
タイトル行へ移動
うまく動かない
code:script.js
moustrapOnEdit.bind('g g', e =>{
_log(${e.key} is pressed.);
if (mode !== 'normal') return;
e.stopPropagation();
e.preventDefault();
simulateKeyPress('home');
editor.getElementsByClassName('line-title')?.0.click(); });
keyだと動かない
code:script.js
const KEY_MAP = {
backspace:8,
tab: 9 ,
enter: 13,
del: 46,
esc: 27,
space: 32,
pageup: 33,
pagedown: 34,
end: 35,
home: 36,
left: 37,
up: 38,
right: 39,
down: 40,
v: 86,
z: 90,
};
function simulateKeyPress(key, {shiftKey = false, ctrlKey = false, altKey = false} = {}) {
cursor.dispatchEvent(new KeyboardEvent('keydown',
{bubbles: true, cancelable: true, keyCode: KEY_MAPkey, shiftKey: shiftKey,ctrlKey: ctrlKey, altKey: altKey})); }
検索
code:script.js
moustrapOnEdit.bind('/', e =>{
_log(${e.key} is pressed.);
if (mode !== 'normal') return;
e.stopPropagation();
e.preventDefault();
simulateKeyPress('f', {ctrlKey: true});
});
indent操作
code:script.js
moustrapOnEdit.bind('< <', e =>{
_log(${e.key} is pressed.);
if (mode !== 'normal') return;
e.stopPropagation();
e.preventDefault();
simulateKeyPress('home');
simulateKeyPress('tab', {shiftKey: true});
});
moustrapOnEdit.bind('> >', e =>{
_log(${e.key} is pressed.);
if (mode !== 'normal') return;
e.stopPropagation();
e.preventDefault();
simulateKeyPress('home');
simulateKeyPress('tab');
});
code:script.js
moustrapOnEdit.bind('ctrl+j', e =>{
_log(${e.key} is pressed.);
if (mode !== 'normal') return;
e.stopPropagation();
e.preventDefault();
simulateKeyPress('down', {ctrlKey: true});
});
moustrapOnEdit.bind('ctrl+k', e =>{
_log(${e.key} is pressed.);
if (mode !== 'normal') return;
e.stopPropagation();
e.preventDefault();
simulateKeyPress('up', {ctrlKey: true});
});
moustrapOnEdit.bind('alt+h', e =>{
_log(${e.key} is pressed.);
if (mode !== 'normal') return;
e.stopPropagation();
e.preventDefault();
simulateKeyPress('left', {altKey: true});
});
moustrapOnEdit.bind('alt+j', e =>{
_log(${e.key} is pressed.);
if (mode !== 'normal') return;
e.stopPropagation();
e.preventDefault();
simulateKeyPress('down', {altKey: true});
});
moustrapOnEdit.bind('alt+k', e =>{
_log(${e.key} is pressed.);
if (mode !== 'normal') return;
e.stopPropagation();
e.preventDefault();
simulateKeyPress('up', {altKey: true});
});
moustrapOnEdit.bind('alt+l', e =>{
_log(${e.key} is pressed.);
if (mode !== 'normal') return;
e.stopPropagation();
e.preventDefault();
simulateKeyPress('right', {altKey: true});
});
切り取り
うまく動かない
code:script.js
moustrapOnEdit.bind('d d', e =>{
_log(${e.key} is pressed.);
if (mode !== 'normal') return;
e.stopPropagation();
e.preventDefault();
const cursorLine = editor.getElementsByClassName('cursor-line')0; const text = cursorLine.innerText.trim();
_log(innerText: ${text});
if(navigator.clipboard){
navigator.clipboard.writeText(text);
}
simulateKeyPress('home');
simulateKeyPress('home');
deleteText(text);
simulateKeyPress('backspace');
simulateKeyPress('home');
simulateKeyPress('home');
});
function deleteText(text) {
for(const _ of text.match(/./ug)) {
simulateKeyPress('del');
}
}
moustrapOnEdit.bind('x', e =>{
_log(${e.key} is pressed.);
if (mode !== 'normal') return;
e.stopPropagation();
e.preventDefault();
simulateKeyPress('del');
});
貼り付け
代わりにctrl+Vを実行する
↑これもうまく動かない
code:script.js
moustrapOnEdit.bind('p', e =>{
_log(${e.key} is pressed.);
if (mode !== 'normal') return;
e.stopPropagation();
e.preventDefault();
simulateKeyPress('v', {ctrlKey: true});
});
戻る
code:script.js
moustrapOnEdit.bind('u', e =>{
_log(${e.key} is pressed.);
if (mode !== 'normal') return;
e.stopPropagation();
e.preventDefault();
simulateKeyPress('z', {ctrlKey:true});
});
IMEを握りつぶす
code:script.js_disabled
cursor.addEventListener('compositionend', e =>{
console.log('End composition: %o', e.data);
if (!e.data) return;
if (mode !== 'normal') return;
deleteText(e.data);
});
うまく行かなかった
IMEの入力をするとEscで握りつぶすようにする
code:script.js
cursor.addEventListener('compositionstart', e =>{
_log('IME can\'t be used in the Normal mode');
if (mode !== 'normal') return;
simulateKeyPress('esc');
});
escが反応しない?
別の手段を使う必要がありそう
リンクを押す
code:script.js
moustrapOnEdit.bind('ctrl+]', e =>{
_log(${e.key} is pressed.);
e.stopPropagation();
e.preventDefault();
if (mode !== 'normal') return;
_log(Searching for the link under the cursor...);
const targetLink = getLinkIncludingCursor();
if (!targetLink) {
_log('No link found.');
return;
}
_log('Target link: %o', targetLink);
targetLink.click();
});
これ動かない
this._handleKey is undefinedになってしまう
code:script.js_disabled
var originalHandleKey = moustrapOnEdit.handleKey;
moustrapOnEdit.handleKey = (character, modifiers, e) =>{
_log(The current mode is ${mode} mode.);
// 文字入力を握りつぶす
if (character.match(/^.$/) && mode === 'normal') {
e.stopPropagation();
e.preventDefault();
}
e.stopPropagation();
e.preventDefault();
}
return originalHandleKey(character, modifiers, e);
};
code:script.js
function moveInsertMode() {
mode = 'insert';
editor.getElementsByClassName('cursor')?.0.classList.remove('normal-mode'); _log(Moved to ${mode} mode.);
}
function moveNormalMode() {
mode = 'normal';
editor.getElementsByClassName('cursor')?.0.classList.add('normal-mode'); _log(Moved to ${mode} mode.);
}
function _log(msg, ...objects){
if (objects.length > 0) console.log([scrapbox-vim-bindings] ${msg}, objects);
console.log([scrapbox-vim-bindings] ${msg});
}
code:script.js
function isFirefox() {
const userAgent = window.navigator.userAgent.toLowerCase();
return userAgent.indexOf('firefox') != -1;
}